<template>
  <div ref="parentRef">
    <div
      class="canvas"
      ref="selfRef"
      @click="onClick"
      :style="style"
      @pointerdown="handleMousedown()"
    >
      <slot></slot>
    </div>
    <div class="anchor" v-if="active" :style="style" @pointerdown="handleMousedown()">
      <div
        v-for="(item, key) in handles"
        :draggable="false"
        @pointerdown.stop="handleMousedown(item)"
        :class="[item, 'anchor-handle']"
        :key="key"
      ></div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { clamp, onClickOutside } from '@vueuse/core';

import { add, subtract } from 'mathjs';
import { computed, onMounted, ref, warn, watch, type CSSProperties } from 'vue';
import type { Handles, ICanvasAttr, IPosition } from './types';
import { toFixed } from './utils';

/* 记录临时坐标 */
const tempPosition: IPosition = {
  x: 0,
  y: 0,
  w: 0,
  h: 0,
  rotate: 0,
};
// HTML REF
const parentRef = ref<HTMLElement>();
const selfRef = ref<HTMLElement>();

// HTML REF
const positionModel = defineModel<IPosition>('position');

const emit = defineEmits(['pointerdown', 'moving', 'resizing', 'handleUp', 'click']);

const handles = ref<Handles[]>(['mr', 'mb', 'mt', 'ml', 'tl', 'tr', 'br', 'bl']);
/* 是否被激活选中 */
const active = ref(false);

const props = withDefaults(defineProps<ICanvasAttr>(), {
  scale: 1,
  draggable: true,
  resizable: true,
  gridX: 1,
  gridY: 1,
});

/**
 * 当前画布的宽高异常的时候，显示警告
 */
const showWarn = () => {
  const parentNode = getParentRectInfo();
  if (props.allowOutOfBounds) {
    if (
      !parentNode ||
      parentNode.width === 0 ||
      parentNode.height === 0 ||
      (positionModel.value &&
        (parentNode.height < positionModel.value!.h || parentNode.width < positionModel.value?.w))
    ) {
      warn(`父级-[宽|高]-异常`);
    }
  }
};
type PositionKey = keyof IPosition;
// 初始化
const setTempPositionByKey = (key: PositionKey, value: number) => {
  if (!positionModel.value) return;
  tempPosition[key] = value;
};

const setTempPosition = () => {
  let keyMap: PositionKey[] = ['x', 'y', 'w', 'h', 'rotate'];
  keyMap.forEach(k => {
    setTempPositionByKey(k, positionModel.value![k]);
  });
};

onMounted(() => {
  setTempPosition();
  showWarn();
});
/* 获取父级ID */
const getParentRectInfo = (): null | DOMRect => {
  if (parentRef.value) {
    const parentNode = parentRef.value.parentElement;
    if (parentNode && props.allowOutOfBounds && positionModel.value) {
      return parentNode.getBoundingClientRect();
    }
  }
  return null;
};

const onClick = () => {
  emit('click', positionModel.value);
};
/* 拖拽调整大小的时候，自动隐藏/显示手柄 */
const autoSetHandles = () => {
  if (!positionModel.value) return;
  const commonHandle: Handles[] = ['tl', 'tr', 'bl', 'br'];
  if (positionModel.value.w < 50 && positionModel.value.h < 50) {
    handles.value = commonHandle;
  } else if (positionModel.value.w <= 50) {
    handles.value = [...commonHandle, 'ml', 'mr'];
  } else if (positionModel.value.h <= 50) {
    handles.value = [...commonHandle, 'mt', 'mb'];
  } else {
    handles.value = ['mr', 'mb', 'mt', 'ml', 'tl', 'tr', 'br', 'bl'];
  }
};

const snapX = (p: number) => Math.round(p / props.gridX) * props.gridX;
const snapY = (p: number) => Math.round(p / props.gridY) * props.gridY;

/**
 * 鼠标按下拖拽
 * 如果flag为空，代表拖拽整个画布
 * @param flag 对应的手柄
 *  */
const handleMousedown = (flag?: Handles) => {
  if (!positionModel.value) return;
  const de = window.event as MouseEvent;
  active.value = true;
  emit('pointerdown', positionModel.value);
  document.body.style.userSelect = 'none';
  positionModel.value.x = tempPosition.x;
  positionModel.value.y = tempPosition.y;
  positionModel.value.w = tempPosition.w;
  positionModel.value.h = tempPosition.h;
  const MAX_WIDTH = innerWidth;
  const MAX_HEIGHT = innerHeight;
  const rect = getParentRectInfo();
  const bounds = {
    left: 0,
    top: 0,
    width: rect?.width || 0,
    height: rect?.height || 0,
  };
  const mousemove = (me: MouseEvent) => {
    showWarn();

    emit('moving', positionModel.value);
    if (!positionModel.value) return;

    const ox = me.x - de.x;
    const oy = me.y - de.y;

    if (!flag) {
      if (props.draggable) {
        const boundX = snapX(add(ox, tempPosition.x));
        const boundY = snapY(add(oy, tempPosition.y));

        positionModel.value.x = props.allowOutOfBounds
          ? clamp(boundX, 0, subtract(bounds.width, positionModel.value.w))
          : boundX;

        positionModel.value.y = props.allowOutOfBounds
          ? clamp(boundY, 0, bounds.height - positionModel.value.h)
          : boundY;
      }
    } else {
      const dragMR = () => {
        const boundW = snapX(add(ox, tempPosition.w));
        positionModel.value!.w = clamp(
          boundW,
          0,
          props.allowOutOfBounds ? Math.abs(bounds.width - positionModel.value!.x) : MAX_WIDTH,
        );
        emit('resizing', positionModel.value);
      };
      const dragMT = () => {
        const boundY = add(oy, tempPosition.y);

        const boundH = subtract(tempPosition.h, oy);

        positionModel.value!.y = clamp(
          boundY,
          props.allowOutOfBounds ? 0 : -MAX_HEIGHT,
          add(tempPosition.y, tempPosition.h),
        );
        positionModel.value!.h = clamp(
          boundH,
          0,
          props.allowOutOfBounds ? add(tempPosition.y, tempPosition.h) : MAX_HEIGHT,
        );
        emit('resizing', positionModel.value);
      };
      const dragML = () => {
        const boundX = add(ox, tempPosition.x);
        const boundW = subtract(tempPosition.w, ox);
        positionModel.value!.x = clamp(
          boundX,
          props.allowOutOfBounds ? 0 : -MAX_WIDTH,
          add(tempPosition.x, tempPosition.w),
        );

        positionModel.value!.w = clamp(
          boundW,
          0,
          props.allowOutOfBounds ? add(tempPosition.x, tempPosition.w) : MAX_WIDTH,
        );
        emit('resizing', positionModel.value);
      };
      const dragMB = () => {
        const boundH = snapY(add(oy, tempPosition.h));
        positionModel.value!.h = clamp(
          boundH,
          0,
          props.allowOutOfBounds ? Math.abs(bounds.height - positionModel.value!.y) : MAX_HEIGHT,
        );
        emit('resizing', positionModel.value);
      };
      const funcMap: Record<Handles, () => unknown> = {
        mr: dragMR,
        mb: dragMB,
        mt: dragMT,
        ml: dragML,
        tl: () => {
          dragML();
          dragMT();
        },
        tr: () => {
          dragMT();
          dragMR();
        },
        br: () => {
          dragMR();
          dragMB();
        },
        bl: () => {
          dragML();
          dragMB();
        },
      };
      if (props.resizable) {
        funcMap[flag]?.();
        autoSetHandles();
      }
    }
  };

  const mouseup = () => {
    setTempPosition();

    globalThis.removeEventListener('pointermove', mousemove);
    globalThis.removeEventListener('pointerup', mouseup);
    document.body.style.userSelect = 'initial';
    emit('handleUp', positionModel.value);
  };

  globalThis.addEventListener('pointermove', mousemove);
  globalThis.addEventListener('pointerup', mouseup);
};

const style = computed((): CSSProperties | undefined => {
  if (!positionModel.value) return undefined;
  const { x, w, y, h, rotate } = positionModel.value;

  const { scale } = props;
  const scaleValue = toFixed(scale || 1);
  const left = x / scaleValue;
  const top = y / scaleValue;

  return {
    width: `${toFixed(w / scaleValue)}px`,
    height: `${toFixed(h / scaleValue)}px`,
    transform: `translate(${left}px, ${top}px) rotate(${rotate}deg)`,
    // left: `${left}px`,
    // top: `${top}px`,
    // rotate: `${rotate}deg`,
  };
});

onClickOutside(parentRef, () => {
  active.value = false;
});

watch(active, () => {
  autoSetHandles();
});
/**
 * 通过input或其他方式，在这个组件外修改 positionModel的值后
 * 必须将修改后的值赋给 tempPosition
 */
defineExpose({
  setTempPositionByKey,
});
</script>

<style lang="scss" scoped>
$handle-color: #0ac6ff;
$wh: 6px;
.canvas {
  position: absolute;
  outline: 1px solid #000;
}

.anchor {
  z-index: 1000000;
  pointer-events: none;
  .anchor-handle {
    pointer-events: initial;
    position: absolute;
    background: $handle-color;
    border-radius: 4px;
  }
  position: absolute;
  outline: 1px solid rgb(226, 10, 255);
  .mr {
    right: calc($wh / 2 * -1);
    top: 0;
    height: 24px;
    top: 50%;
    width: $wh;
    transform: translateY(-50%);

    cursor: e-resize;
  }
  .mb {
    cursor: n-resize;
    height: $wh;
    width: 24px;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc($wh / 2 * -1);
  }
  .mt {
    cursor: n-resize;
    height: $wh;
    width: 24px;
    left: 50%;
    transform: translateX(-50%);
    top: calc($wh / 2 * -1);
  }
  .ml {
    cursor: e-resize;
    height: 24px;
    top: 50%;
    width: $wh;
    transform: translateY(-50%);
    left: calc($wh / 2 * -1);
  }
  .tl {
    cursor: nw-resize;
    height: calc($wh * 2);
    width: calc($wh * 2);
    top: -$wh;
    left: -$wh;
  }
  .tr {
    cursor: ne-resize;
    height: calc($wh * 2);
    width: calc($wh * 2);
    top: -$wh;
    right: -$wh;
  }
  .br {
    cursor: se-resize;
    height: calc($wh * 2);
    width: calc($wh * 2);
    bottom: -$wh;
    right: -$wh;
  }
  .bl {
    cursor: sw-resize;
    height: calc($wh * 2);
    width: calc($wh * 2);
    bottom: -$wh;
    left: -$wh;
  }
}
</style>
